package com.xuecheng.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.base.model.dto.PageParams;
import com.xuecheng.base.model.dto.PageResult;
import com.xuecheng.base.model.dto.RestResponse;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.mapper.MediaProcessMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.model.po.MediaProcess;
import com.xuecheng.media.service.MediaFileService;
import io.minio.*;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author Mr.M
 * @version 1.0
 * @description TODO
 * @date 2022/9/10 8:58
 */
@Service
@Slf4j
public class MediaFileServiceImpl implements MediaFileService {

    @Value("${minio.bucket.files}")
    private String bucket;

    @Value("${minio.bucket.videofiles}")
    private String videoBucket;

    @Autowired
    MediaFilesMapper mediaFilesMapper;

    @Autowired
    MinioClient minioClient;

    @Autowired
    MediaFileService currentPorxy;

    @Autowired
    MediaProcessMapper mediaProcessMapper;

    @Override
    public PageResult<MediaFiles> queryMediaFiles(Long companyId, PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {

        //构建查询条件对象
        LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();

        //分页对象
        Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
        // 查询数据内容获得结果
        Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
        // 获取数据列表
        List<MediaFiles> list = pageResult.getRecords();
        // 获取数据总数
        long total = pageResult.getTotal();
        // 构建结果集
        PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
        return mediaListResult;

    }

    /**
     * 上传文件
     *
     * @param companyId
     * @param localPtah           本地文件路径
     * @param uploadFileParamsDto 上传参数模型
     * @return
     */
    @Override
    @Transactional
    public UploadFileResultDto upload(Long companyId, String localPtah, UploadFileParamsDto uploadFileParamsDto, String objectName) {
        File file = new File(localPtah);
        if (uploadFileParamsDto == null) {
            XueChengPlusException.cast("上传参数为空");
        }
        if (!file.exists()) {
            XueChengPlusException.cast("文件的本地路径为空");
        }
        //获取mimeType
        String filename = uploadFileParamsDto.getFilename();
        //文件后缀名
        String extension = filename.substring(filename.lastIndexOf("."));
        String mimeType = getMimeType(extension);
        //创建objectName:/+YYYY/MM/DD/MD5/extension
        String defaultFolderPath = getDefaultFolderPath();
        String fileMD5 = getMD5(file);
        if (StringUtils.isEmpty(objectName)) {
            objectName = defaultFolderPath + fileMD5 + extension;
        }
        //上传到minio
        boolean addminIO = addMediaFilesToMinIO(bucket, mimeType, objectName, localPtah);
        if (!addminIO) {
            XueChengPlusException.cast("上传Minio失败");
            return null;
        }
        //添加一条记录到mediaFile
        //文件大小
        uploadFileParamsDto.setFileSize(file.length());
        MediaFiles mediaFiles = currentPorxy.addMediaFilesToDB(fileMD5, bucket, objectName, uploadFileParamsDto, companyId);
        UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
        BeanUtils.copyProperties(mediaFiles, uploadFileResultDto);
        return uploadFileResultDto;
    }

    /**
     * 根据md5检查文件是否存在
     * 1、数据库是否存在
     * 2、minio是否存在
     *
     * @param fileMd5
     * @return
     */
    @Override
    public RestResponse<Boolean> checkFile(String fileMd5) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles == null) {
            return RestResponse.success(false);
        }
        String filesBucket = mediaFiles.getBucket();
        String filePath = mediaFiles.getFilePath();
        GetObjectArgs build = GetObjectArgs.builder().object(filePath).bucket(filesBucket).build();
        FilterInputStream inputStream = null;
        try {
            inputStream = minioClient.getObject(build);
        } catch (Exception e) {
            e.printStackTrace();
            return RestResponse.success(false);
        }
        if (inputStream != null) {
            return RestResponse.success(true);
        }
        return RestResponse.success(false);
    }

    /**
     * 根据MD5查询分块文件
     *
     * @param fileMd5
     * @return
     */
    @Override
    public RestResponse<Boolean> checkChunk(String fileMd5) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
        if (mediaFiles == null) {
            return RestResponse.success(false);
        }
        String filesBucket = mediaFiles.getBucket();
        String filePath = mediaFiles.getFilePath();
        GetObjectArgs build = GetObjectArgs.builder().bucket(filesBucket).object(filePath).build();
        FilterInputStream filterInputStream = null;
        try {
            filterInputStream = minioClient.getObject(build);
        } catch (Exception e) {
            e.printStackTrace();
        }
        if (filterInputStream == null) {
            return RestResponse.success(false);
        }

        return RestResponse.success(true);
    }

    /**
     * 上传分块
     *
     * @param localChunkFilePath 文件本地路径
     * @param fileMd5
     * @param chunkIndex
     * @return
     */
    @Override
    @Transactional
    public RestResponse<Boolean> uploadChunk(String localChunkFilePath, String fileMd5, int chunkIndex) {
        String chunkDefaultPath = getChunkDefaultPath(fileMd5);
        String objectName = chunkDefaultPath + chunkIndex;
        String mimeType = getMimeType(null);
        boolean b = addMediaFilesToMinIO(videoBucket, mimeType, objectName, localChunkFilePath);
        if (!b) {
            log.debug("上传分块文件失败，分块文件本地路径：{}", localChunkFilePath);
            XueChengPlusException.cast("上传分块文件失败");
        }
        log.debug("上传分块文件成功，分块文件本地路径:", localChunkFilePath);
        return RestResponse.success(true);
    }

    /**
     * 合并分块
     * 下载合并后的文件
     * MD5比对
     * 合并后的文件入库，
     * 清理分块
     *
     * @param companyId
     * @param uploadFileParamsDto
     * @param chunkTotal
     * @param fileMd5
     * @return
     */
    @Override
    @Transactional
    public RestResponse mergeChunks(Long companyId, UploadFileParamsDto uploadFileParamsDto, int chunkTotal, String fileMd5) {
        String chunkDefaultPath = getChunkDefaultPath(fileMd5);
        //合并，遍历minion分块文件，封装到集合
        List<ComposeSource> sources = Stream.iterate(0, i -> ++i)
                .limit(chunkTotal)
                .map(i -> ComposeSource
                        .builder()
                        .bucket(videoBucket)
                        .object(chunkDefaultPath.concat(Integer.toString(i)))//每一个分块的路径
                        .build())
                .collect(Collectors.toList());

        String filename = uploadFileParamsDto.getFilename();
        String extension = filename.substring(filename.lastIndexOf("."));
        String objectName = getFilePathByMd5(fileMd5, extension);
        //
        ComposeObjectArgs composeObjectArgs = ComposeObjectArgs
                .builder()
                .sources(sources)//minio遍历的集合放入
                .object(objectName)//文件合并后的绝对路径
                .bucket(videoBucket)
                .build();
        try {
            minioClient.composeObject(composeObjectArgs);
        } catch (Exception e) {
            e.printStackTrace();
            log.debug("合并分块失败，fileMd5:{}", fileMd5);
            XueChengPlusException.cast("合并分块失败");
            return RestResponse.validfail("合并分块失败");
        }
        // ====验证md5====
        //下载合并后的文件
        File minioFile = downloadFileFromMinIO(fileMd5, extension, videoBucket);
        if (minioFile == null) {
            log.debug("下载合并后文件失败,fileMd5:{}", fileMd5);
            return RestResponse.validfail(false, "下载合并后文件失败。");
        }

        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream(minioFile);
            String s = DigestUtils.md5Hex(fileInputStream);
            if (!s.equals(fileMd5)) {
                log.debug("上传文件失败：fileMd5:{}", fileMd5);
                return RestResponse.validfail("上传文件失败");
            }
            //设置文件大小
            uploadFileParamsDto.setFileSize(minioFile.length());
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                fileInputStream.close();
            } catch (IOException e) {
                log.debug("校验文件失败,fileMd5:{},异常:{}", fileMd5, e.getMessage(), e);
                return RestResponse.validfail(false, "效验文件失败，最终上传失败");
            }
        }
        //入库
        currentPorxy.addMediaFilesToDB(fileMd5, videoBucket, objectName, uploadFileParamsDto, companyId);
        //清除分块文件
        clearChunkFiles(chunkTotal, chunkDefaultPath, videoBucket);
        return RestResponse.success(true);

    }

    /**
     * 清楚chunk分块文件
     *
     * @param chunkTotal
     * @param objectName
     * @param bucket
     * @return
     */
    public boolean clearChunkFiles(int chunkTotal, String objectName, String bucket) {
        List<DeleteObject> collect = Stream.iterate(0, i -> ++i)
                .limit(chunkTotal)
                .map(i -> new DeleteObject(objectName.concat(Integer.toString(i))))
                .collect(Collectors.toList());
        RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder().bucket(bucket).objects(collect).build();
        Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
        results.forEach(i -> {
            try {
                DeleteError deleteError = i.get();
            } catch (Exception e) {
                log.debug("删除chunk文件失败，最终上传文件失败，objectName:{},bucket:{}", objectName, bucket, e.getMessage());
                return;
            }
        });
        log.debug("删除chunk文件成功，objectName:{},bucket:{}", objectName, bucket);
        return true;
    }

    /**
     * 下载minio文件
     *
     * @param fileMd5
     * @param extension
     * @return
     */

    public File downloadFileFromMinIO(String fileMd5, String extension, String bucket) {
        File minio = null;
        FileOutputStream outputStream = null;
        //objectName,文件在minio中的绝对路径
        String objectName = getFilePathByMd5(fileMd5, extension);
        GetObjectArgs build = GetObjectArgs.builder().bucket(bucket).object(objectName).build();
        try {
            InputStream inputStream = minioClient.getObject(build);
            minio = File.createTempFile("minio", ".temp");
            outputStream = new FileOutputStream(minio);
            //把输入流拷贝到输出流，流中的文件也会拷贝
            IOUtils.copy(inputStream, outputStream);
            log.debug("下载文件成功,fileMd5:{},objectName:{}", fileMd5, objectName);
            return minio;
        } catch (Exception e) {
            e.printStackTrace();
            log.debug("下载文件失败,fileMd5:{},objectName:{}", fileMd5, objectName);
            XueChengPlusException.cast("下载文件失败");
            return null;
        }

    }

    /**
     * 合并时要指定objectName是在分块文件路径的上一层，获取该路径
     *
     * @param fileMd5
     * @param extension
     * @return
     */
    private String getFilePathByMd5(String fileMd5, String extension) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + extension;
    }

    /**
     * 分块的默认存储路径
     *
     * @param fileMd5
     * @return
     */
    private String getChunkDefaultPath(String fileMd5) {
        String chunkDefaultPath = fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
        return chunkDefaultPath;
    }

    /**
     * 添加文件信息到数据库，返回值设为MediaFiles，返回前端，组织UploadFileResultDto数据
     *
     * @param fileMD5             文件md5值
     * @param bucket              桶
     * @param objcetName          上传minio中的绝对路径
     * @param uploadFileParamsDto
     * @param companyId
     * @return
     */
    @Transactional
    public MediaFiles addMediaFilesToDB(String fileMD5, String bucket, String objcetName, UploadFileParamsDto uploadFileParamsDto, Long companyId) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMD5);
        if (mediaFiles != null) {
            return mediaFiles;
        }
        mediaFiles = new MediaFiles();
        mediaFiles.setFileId(fileMD5);
        mediaFiles.setFilePath(objcetName);
        mediaFiles.setFileSize(uploadFileParamsDto.getFileSize());
        mediaFiles.setUrl("/" + bucket + "/" + objcetName);
        mediaFiles.setStatus("1");
        mediaFiles.setAuditStatus("202003");
        mediaFiles.setCreateDate(LocalDateTime.now());
        mediaFiles.setCompanyId(companyId);
        mediaFiles.setId(fileMD5);
        mediaFiles.setBucket(bucket);
        mediaFiles.setFilename(uploadFileParamsDto.getFilename());
        mediaFiles.setFileType(uploadFileParamsDto.getFileType());
        mediaFiles.setUsername(uploadFileParamsDto.getUsername());
        mediaFiles.setTags(uploadFileParamsDto.getTags());
        mediaFiles.setRemark(uploadFileParamsDto.getRemark());
        int insert = mediaFilesMapper.insert(mediaFiles);
        if (insert <= 0) {
            log.error("保存文件信息到数据库失败,{}", mediaFiles.toString());
            XueChengPlusException.cast("保存文件信息失败");

        }
        addWaitTask(mediaFiles);
        return mediaFiles;
    }

    /**
     * 添加待处理任务
     *
     * @param mediaFiles
     */
    public void addWaitTask(MediaFiles mediaFiles) {
        String fileMd5 = mediaFiles.getId();
        String filename = mediaFiles.getFilename();
        String extension = filename.substring(filename.lastIndexOf("."));
        String mimeType = getMimeType(extension);


        if (!"video/x-msvideo".equals(mimeType)) {
            return;
        }
        MediaProcess mediaProcess = new MediaProcess();
        BeanUtils.copyProperties(mediaFiles, mediaProcess);
        mediaProcess.setStatus("1");
        mediaProcess.setFailCount(0);
        mediaProcessMapper.insert(mediaProcess);

    }

    /**
     * 获取md5值失败
     *
     * @param file
     * @return
     */
    public String getMD5(File file) {
        try {
            FileInputStream fileInputStream = new FileInputStream(file);
            return DigestUtils.md5Hex(fileInputStream);
        } catch (Exception e) {
            e.printStackTrace();
            log.debug("获取md5值失败");
            return null;
        }
    }

    /**
     * 获取年/月/日/格式存储minio
     *
     * @return
     */
    public String getDefaultFolderPath() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
        String folder = simpleDateFormat.format(new Date()).replace("-", "/") + "/";
        return folder;
    }

    /**
     * 获取文件memetype，用于上传文件
     *
     * @param extension
     * @return
     */
    public String getMimeType(String extension) {

        if (extension == null)
            extension = "";
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType，字节流
        if (extensionMatch != null) {
            return extensionMatch.getMimeType();
        }
        return mimeType;
    }

    /**
     * 文件添加到minio
     *
     * @param bucket
     * @param mimeType
     * @param objectName
     * @param localPath
     * @return
     */
    public boolean addMediaFilesToMinIO(String bucket, String mimeType, String objectName, String localPath) {
        try {
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs
                    .builder()
                    .object(objectName)
                    .bucket(bucket)
                    .filename(localPath)
                    .contentType(mimeType)
                    .build();
            minioClient.uploadObject(uploadObjectArgs);
            log.debug("上传文件成功:bucket:{},objectName:{},localPath:{}", bucket, objectName, localPath);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.debug("上传文件失败:bucket:{},objectName:{},localPath:{}", bucket, objectName, localPath);
            XueChengPlusException.cast("上传文件失败");
            return false;
        }

    }

    /**
     * @param MediaId
     * @return
     */
    @Override
    public MediaFiles getFileById(String MediaId) {
        MediaFiles mediaFiles = mediaFilesMapper.selectById(MediaId);
        return mediaFiles;
    }
}
